﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Ls.Data
{
    public static class DbProviderFactories
    {
        private struct ProviderRegistration
        {
            internal ProviderRegistration(string factoryTypeAssemblyQualifiedName, DbProviderFactory factoryInstance)
            {
                this.FactoryTypeAssemblyQualifiedName = factoryTypeAssemblyQualifiedName;
                this.FactoryInstance = factoryInstance;
            }

            internal string FactoryTypeAssemblyQualifiedName { get; }
            /// <summary>
            /// The cached instance of the type in <see cref="FactoryTypeAssemblyQualifiedName"/>. If null, this registation is seen as a deferred registration
            /// and <see cref="FactoryTypeAssemblyQualifiedName"/> is checked the first time when this registration is requested through GetFactory().
            /// </summary>
            internal DbProviderFactory FactoryInstance { get; }
        }

        static DbProviderFactories() {
            var types = Ls.Utils.ReflectionHelper.FindClassesOfType<DbProviderFactory>();
            if (types != null && types.Count >0)
            {
                foreach (var t in types)
                {
                    RegisterFactory(t.Namespace,t);
                }
            }
        }

        private static ConcurrentDictionary<string, ProviderRegistration> _registeredFactories = new ConcurrentDictionary<string, ProviderRegistration>();
        private const string AssemblyQualifiedNameColumnName = "AssemblyQualifiedName";
        private const string InvariantNameColumnName = "InvariantName";
        private const string NameColumnName = "Name";
        private const string DescriptionColumnName = "Description";
        private const string ProviderGroupColumnName = "DbProviderFactories";
        private const string InstanceFieldName = "Instance";

        public static bool TryGetFactory(string providerInvariantName, out DbProviderFactory factory)
        {
            factory = GetFactory(providerInvariantName, throwOnError: false);
            return factory != null;
        }

        public static DbProviderFactory GetFactory(string providerInvariantName)
        {

           
            return GetFactory(providerInvariantName, throwOnError: true);
        }
 
         
        public static IEnumerable<string> GetProviderInvariantNames()
        {
            return _registeredFactories.Keys.ToList();
        }

        public static void RegisterFactory(string providerInvariantName, string factoryTypeAssemblyQualifiedName)
        {
            
            // this method performs a deferred registration: the type name specified is checked when the factory is requested for the first time. 
            _registeredFactories[providerInvariantName] = new ProviderRegistration(factoryTypeAssemblyQualifiedName, null);
        }

        public static void RegisterFactory(string providerInvariantName, Type providerFactoryClass)
        {
            RegisterFactory(providerInvariantName, GetFactoryInstance(providerFactoryClass));
        }

        public static void RegisterFactory(string providerInvariantName, DbProviderFactory factory)
        { 
            _registeredFactories[providerInvariantName] = new ProviderRegistration(factory.GetType().AssemblyQualifiedName, factory);
        }

        public static bool UnregisterFactory(string providerInvariantName)
        {
            return !string.IsNullOrWhiteSpace(providerInvariantName) && _registeredFactories.TryRemove(providerInvariantName, out _);
        }

        private static DbProviderFactory GetFactory(string providerInvariantName, bool throwOnError)
        {
            if (string.IsNullOrWhiteSpace(providerInvariantName))
            {
                return null;
            }
            bool wasRegistered = _registeredFactories.TryGetValue(providerInvariantName, out ProviderRegistration registration);
            if (!wasRegistered)
            { 
                return null;
            }
            //MySql.Data.MySqlClient.MySqlClientFactory
            //_registeredFactories.GetOrAdd()
            DbProviderFactory toReturn = registration.FactoryInstance;
            if (toReturn == null)
            {
                // Deferred registration, do checks now on the type specified and register instance in storage.
                // Even in the case of throwOnError being false, this will throw when an exception occurs checking the registered type as the user has to be notified the 
                // registration is invalid, even though the registration is there.
                toReturn = GetFactoryInstance(GetProviderTypeFromTypeName(registration.FactoryTypeAssemblyQualifiedName));
                RegisterFactory(providerInvariantName, toReturn);
            }
            return toReturn;
        }

        private static DbProviderFactory GetFactoryInstance(Type providerFactoryClass)
        { 
            if (!providerFactoryClass.IsSubclassOf(typeof(DbProviderFactory)))
            {
                return null;
            }

            FieldInfo providerInstance = providerFactoryClass.GetField(InstanceFieldName, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
            if (null == providerInstance)
            {
                return null;
            }
            if (!providerInstance.FieldType.IsSubclassOf(typeof(DbProviderFactory)))
            {
                return null;
            }
            object factory = providerInstance.GetValue(null);
            if (null == factory)
            {
                return null;
            }
            return (DbProviderFactory)factory;
        }


        private static Type GetProviderTypeFromTypeName(string assemblyQualifiedName)
        {
            Type providerType = Type.GetType(assemblyQualifiedName);
            if (null == providerType)
            {
                return null;
            }
            return providerType;
        }
    }
}
